package br.com.dyad.commons.reflect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import br.com.dyad.commons.data.AppEntity;

public class ReflectUtil {

	public static HashMap<Class, Class> primitiveWrapper;
	static {
		primitiveWrapper = new HashMap<Class, Class>();
		primitiveWrapper.put(int.class, Integer.class);
		primitiveWrapper.put(short.class, Short.class);
		primitiveWrapper.put(byte.class, Byte.class);
		primitiveWrapper.put(long.class, Long.class);
		primitiveWrapper.put(float.class, Float.class);
		primitiveWrapper.put(double.class, Double.class);
		primitiveWrapper.put(boolean.class, Boolean.class);
		primitiveWrapper.put(char.class, Character.class);
	}
	public static final String PROPERTY_ACCESSOR = "get";
	
	@SuppressWarnings("unchecked")
	public static List<Annotation> getAnnotations(Class annotation) {
		ArrayList<Annotation> list = new ArrayList<Annotation>();

		for (Annotation temp : annotation.getAnnotations()) {
			list.add(temp);
		}

		return list;
	}

	@SuppressWarnings("unchecked")
	public static List<String> getClassProperties(Class classRef, boolean getSuperclassProperties) {
		List<String> list = new ArrayList<String>();

		for (Field field : classRef.getDeclaredFields()) {
			String name = field.getName();
			if ( ( ! isTransient(field) && name.indexOf('$') == -1 ) || name.equals("id") /* Verificar com Helton*/ ){
			//if ( ! isTransient(field) && name.indexOf('$') == -1 ){
				list.add(name);
			}			
		}

		// Add superclass fields
		if ( getSuperclassProperties && classRef.getSuperclass() != Object.class) {
			list.addAll(getClassProperties(classRef.getSuperclass(), getSuperclassProperties));
		}

		return list;
	}
	
	@SuppressWarnings("unchecked")
	/**
	 * Pega recursivamente uma um field na hierquia das classes. 
	 * Este m�todo � recursivo.
	 * */
	public static Field getDeclaredField(Class clazz, String name) throws RuntimeException{
		try {
			Field field = clazz.getDeclaredField(name);
			return field;
		} catch (Exception e) {
			if (clazz != Object.class && clazz.getSuperclass() != Object.class){
				return getDeclaredField(clazz.getSuperclass(), name);
			} else {
				throw new RuntimeException(e);
			}
		}
	}
	/**
	 * Criado por Haron - 04/11/2009
	 * Esse m�todo retorna o nome real do campo ignorando o case sensitive do Java.
	 * Ex: codigotitular da classe Beneficiario, retorna codigoTitular, que � o nome real.
	 * @param clazz
	 * @param name
	 * @return
	 * @throws RuntimeException
	 */
	public static String getNameFieldIgnoreCase(Class clazz, String name) throws RuntimeException{
		List<Field> fieldsClass = getClassFields(clazz, true);
		String nameField = "";
		for (Iterator iterator = fieldsClass.iterator(); iterator.hasNext();) {
			Field field = (Field) iterator.next();
			if (name.equalsIgnoreCase(field.getName()) ){
				nameField = field.getName();
				break;
			}
		}
		
		if (nameField == null || nameField.isEmpty() ){
			throw new RuntimeException("Atribute " + name + " does not exist in class "+clazz.getName());
		}
		
		return nameField;	
	}	
	
	public static boolean isTransient( Field field ){
		return Modifier.isTransient(field.getModifiers());
	}
	
	@SuppressWarnings("unchecked")
	public static List<Field> getClassFields(Class classRef, boolean getSuperclassProperties) {
		List<Field> list = new ArrayList<Field>();

		for (Field field : classRef.getDeclaredFields()) {
			if (field.getName().indexOf('$') == -1){				
				list.add(field);
			}
		}

		// Add superclass fields
		if (getSuperclassProperties && classRef.getSuperclass() != Object.class) {
			list.addAll(getClassFields(classRef.getSuperclass(),
					getSuperclassProperties));
		}

		return list;
	}
	
	@SuppressWarnings("unchecked")
	public static List<String> getPersistedProperties(Class classRef) {
		List<String> list = new ArrayList<String>();
		List<Field> fields = getClassFields(classRef, true);

		for (Field field : fields) {
			int modifiers = field.getModifiers();
			if ( ! Modifier.isTransient(modifiers) && ! Modifier.isStatic(modifiers) ){
				list.add(field.getName());
			}
		}
		
		return list;
	}

	@SuppressWarnings("unchecked")
	public static List<String> getClassTypesProperties(Class classRef,
			boolean getSuperclassProperties) {
		List<String> list = new ArrayList<String>();

		for (Field field : classRef.getDeclaredFields()) {
			list.add(field.getType().getName() + "," + field.getName());
		}

		// Add superclass fields
		if (getSuperclassProperties && classRef.getSuperclass() != Object.class) {
			list.addAll(getClassTypesProperties(classRef.getSuperclass(),
					getSuperclassProperties));
		}

		return list;
	}

	@SuppressWarnings("unchecked")
	public static List<String> getClassMethods(Class classRef,
			boolean getSuperclassMethods) {
		List<String> list = new ArrayList<String>();

		for (Method method : classRef.getDeclaredMethods()) {
			list.add(method.getName());
		}

		// Add superclass fields
		if (getSuperclassMethods && classRef.getSuperclass() != Object.class) {
			list.addAll(getClassMethods(classRef.getSuperclass(),
					getSuperclassMethods));
		}

		return list;
	}
	
	@SuppressWarnings("unchecked")
	public static Class getFieldType(Class clazz, String fieldName) throws RuntimeException {
		try{			
			Field field = ReflectUtil.getDeclaredField(clazz,fieldName);
			return field.getType();
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public static Object getFieldValue(Object object, String fieldName){
		if ( object == null ){
			return null;
			/*
			   --------------------------------------------------------------------
			   Modificações: a exceção abaixo foi comentada pois estava gerando um erro no lookup
			   quando não retornava ocorrência e o parâmetro object é nul
			   
			   Desen.  			Data      	Requisito
			   --------------------------------------------------------------------
			   Danilo Sampaio 	05/01/2010  
			   --------------------------------------------------------------------  

			*/
			//throw new RuntimeException("Object cannot be null.");
		}

		String methodName = "";
		Field field;
		try {
			field = ReflectUtil.getDeclaredField( object.getClass(), fieldName);
			if ( Modifier.isPublic( field.getModifiers() ) ){
				methodName = fieldName;  
				return field.get(object);
			} else {
				methodName = "get" + StringUtils.capitalize(fieldName);  				
				return getMethodReturn(object, methodName);
			}
		} catch (Exception e){
			throw new RuntimeException(e);
		}		
	}
	
	public static void setFieldValue(Object object, String name, Object value) throws RuntimeException {			
		try {
			//BeanUtils.setProperty(object, name, value);			
			name = "set" + StringUtils.capitalize(name);
			Class[] paramsType = null;
			
			if (value == null){										
				Method[] method = object.getClass().getMethods();
				for (Method temp : method) {
					if (temp.getName().equals(name)){
						paramsType = temp.getParameterTypes();
						break;
					}
				}
			} else {
				paramsType = new Class[]{value.getClass()};
			}			
			
			//ReflectUtil.getMethodReturn(object, name, paramsType, new Object[]{value});
			ReflectUtil.getMethodReturn(object, name, new Object[]{value});
			
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public static void populateBean(Object bean, HashMap hashMap)
			throws Exception {		
		BeanUtils.populate(bean, hashMap);	
	}
	
	@SuppressWarnings("unchecked")
	public static Object getStaticAttribute(Class clazz, String propName) throws RuntimeException{
		try{			
			Field field = clazz.getField(propName);
			return field.get(null);
		}catch(Exception e){
			throw new RuntimeException(e);
		}
	}
	
	public static RuntimeException removeInvocationTargetException(Exception e) {
		if (e instanceof InvocationTargetException) {
			Throwable t = ((InvocationTargetException)e).getTargetException();
			
			if (t != null) {	
				return new RuntimeException(t);
			}
		}
		
		return new RuntimeException(e);
	}
	
	public static Object getMethodReturn(Object object, String methodName) {
		try {
			Method method = object.getClass().getMethod(methodName, new Class[]{});
			return method.invoke(object, new Object[]{});
		} catch (Exception e) {
			throw removeInvocationTargetException(e);
		}
	}
	
	@SuppressWarnings("unchecked")
	public static Object getMethodReturn(Class clazz, String methodName, Class[] parameterTypes, Object[] params) throws RuntimeException{
		try {						
			Method method = clazz.getMethod(methodName, parameterTypes);
			return method.invoke(null, params);
		} catch (Exception e) {
			throw removeInvocationTargetException(e);
		}
	}
	
	@SuppressWarnings("unchecked")
	public static Object getMethodReturn(Class clazz, String methodName, Object[] params) throws RuntimeException{
		try {									
			Method[] methods = clazz.getMethods();
			
			int methodsLength = methods.length;
			for (int i = 0; i < methodsLength; i++) {

				Method method = methods[i]; 
				int lengthMethodParameterTypes = method.getParameterTypes().length;
				boolean methodGetNameIsEqualToParamMethodName = method.getName().equals(methodName);
		
				if ( lengthMethodParameterTypes == 0 && (params == null || params.length == 0) && methodGetNameIsEqualToParamMethodName ){
				
					return method.invoke(null);
				
				} else if ( methodGetNameIsEqualToParamMethodName ) {					
					
					if ( lengthMethodParameterTypes == params.length){
						Class[] pTypes = method.getParameterTypes();
						boolean match = true;
												
						for (int j = 0; j < pTypes.length; j++) {
							boolean erro = false;
							Class pType  = pTypes[j];
							Object param = params[j];
							if (param != null){								
								try {
									Object obj = pType.cast(param);
								} catch (Exception e) {
									if (!ReflectUtil.inheritsFrom(param.getClass(), pType)){										
										erro = true;
									}
								}
							}
							
							if (param != null && erro){
								match = false;
								break;
							}
						}
						
						if (match){						
							return getMethodReturn(clazz, methodName, pTypes, params);
						}
					}
				}
			}
		} catch (Exception e) {
			throw removeInvocationTargetException(e);
		}
		
		String spTypes = "";
		for (int i = 0; i < params.length; i++) {
			spTypes += "," + (params[i] == null ? "null" : params[i].getClass().getName());
		}
		spTypes = spTypes.substring(1);
		
		throw new RuntimeException("Method: " + methodName + "(" + spTypes + ") not found on class " + clazz.getName());
	}
	
	@SuppressWarnings("unchecked")
	public static Object getMethodReturn(Object object, String methodName, Class[] parameterTypes, Object[] params) throws RuntimeException{
		try {						
			Method method = object.getClass().getMethod(methodName, parameterTypes);
			Object result = method.invoke(object, params);
			return result;
		} catch (Exception e) {
			throw removeInvocationTargetException(e);
		}
	}
	
	@SuppressWarnings("unchecked")
	public static Object getMethodReturn(Object object, String methodName, Object[] params) throws RuntimeException{
		try {									
			/*if (params == null || params.length == 0){
			}*/
			Method[] methods = object.getClass().getMethods();
			
			int methodsLength = methods.length;
			for (int i = 0; i < methodsLength; i++) {
				Method method = methods[i]; 
				
				boolean methodGetNameIsEqualToParamMethodName = method.getName().equals(methodName);

				if (method.getParameterTypes().length == 0 && (params == null || params.length == 0) && methodGetNameIsEqualToParamMethodName ){
					
					return method.invoke(object);
				
				} else if ( methodGetNameIsEqualToParamMethodName ) {					
					
					if (method.getParameterTypes().length == params.length){
						
						Class[] pTypes = methods[i].getParameterTypes();
						boolean match = true;
						
						for (int j = 0; j < pTypes.length; j++) {
							boolean erro = false;
							if (params[j] != null){								
								try {
									Object obj = pTypes[j].cast(params[j]);
								} catch (Exception e) {
									if (!ReflectUtil.inheritsFrom(params[j].getClass(), pTypes[j])){										
										erro = true;
									}
								}
							}
							
							if (params[j] != null && erro /*&& !ReflectUtil.inheritsFrom(params[j].getClass(), pTypes[j])*/){
								match = false;
								break;
							}
						}
						
						if (match){						
							return getMethodReturn(object, methodName, pTypes, params);
						}
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
		String spTypes = "";
		for (int i = 0; i < params.length; i++) {
			spTypes += "," + (params[i] == null ? "null" : params[i].getClass().getName());
		}
		spTypes = spTypes.substring(1);
		
		throw new RuntimeException("Method: " + methodName + "(" + spTypes + ") not found on class " + object.getClass().getName());
	}
	
	@SuppressWarnings("unchecked")
	public static boolean inheritsFrom(Class baseClass, Class parent){
		if (parent.isPrimitive()){
			parent = primitiveWrapper.get(parent);
		}
		
		if (baseClass.equals(parent)){
			return true;
	    } else if (baseClass.equals(Object.class)){
			return false;
		}				
		
		Class superClass = baseClass; 
		
		do{
			Class[] interfaces = superClass.getInterfaces();
			if (ArrayUtils.indexOf(interfaces, parent) != -1){
				return true; 
			}
			
			superClass = superClass.getSuperclass();
			
			if (superClass != null && superClass.equals(parent)){
				return true;
			}						
		}while(superClass != null && superClass != Object.class);
		
		
		return false;
	}

	public static String getSingleNameFromClassName( String className ) throws Exception{
		if ( className == null || className.equals("")  ){
			throw new Exception( "ClassName can not be null or empty." );
		}		
		String names[] = StringUtils.split(className, ".");
		return names[ names.length - 1 ];
	}
	
	@SuppressWarnings("unchecked")
	public static Object getClassInstance(Class clazz, Object[] params) throws Exception{
		if (params != null && params.length > 0){
			Constructor[] constructors = clazz.getConstructors();
			
			for (int i = 0; i < constructors.length; i++) {
				if (constructors[i].getParameterTypes().length == params.length){
					Class[] pTypes = constructors[i].getParameterTypes();
					boolean match = true;
					
					for (int j = 0; j < pTypes.length; j++) {
						if (params[j] != null && !ReflectUtil.inheritsFrom(params[j].getClass(), pTypes[j])){
							match = false;
							break;
						}
					}
					
					if (match){
						return constructors[i].newInstance(params);
					}
				}
			}
		}
		
		return clazz.newInstance();
	}
	
	@SuppressWarnings("unchecked")
	public static Object getClassInstance(String className, Object[] params) throws Exception{
		Class clazz = Class.forName(className);
		return getClassInstance(clazz, params);
	}
	
	public static Method findMethod(Class clazz, String methodName) {		
		for (Method m : clazz.getMethods()){
			if (m.getName().equals(methodName)){
				return m;
			}
		}	
		
		return null;
	}
	
	public static Method findMethod(Class clazz, String methodName, Object[] params) {
		Method[] methods = clazz.getMethods();
		
		for (int i = 0; i < methods.length; i++) {
			
			Method method = methods[i];
			
			boolean methodGetNameIsEqualToParamMethodName = method.getName().equals(methodName);

			if (method.getParameterTypes().length == 0 && (params == null || params.length == 0) && methodGetNameIsEqualToParamMethodName){
			
				return method;
			
			} else if (methodGetNameIsEqualToParamMethodName) {					
				
				if (method.getParameterTypes().length == params.length){
					Class[] pTypes = methods[i].getParameterTypes();
					boolean match = true;
					
					for (int j = 0; j < pTypes.length; j++) {
						if (params[j] != null && !ReflectUtil.inheritsFrom(params[j].getClass(), pTypes[j])){
							match = false;
							break;
						}
					}
					
					if (match){						
						return method;
					}
				}
			}
		}
		
		return null;
	}

	//TODO Dar prefer�ncia a vari�veis com o mesmo nome (case sensitive)
	public static Object getFieldValueIgnoreCase(Object object, String fieldName) {
		List<String> fieldNames = getClassProperties( object.getClass(), true);
		String methodName = "";
		for (Iterator iterator = fieldNames.iterator(); iterator.hasNext();) {
			String classFieldName = (String) iterator.next();
			if ( classFieldName.equalsIgnoreCase(fieldName) ){
				methodName = "get" + StringUtils.capitalize(classFieldName);  
				break;
			}
		}
		if (methodName == null || methodName.length() == 0 ){
			throw new RuntimeException("Field " + fieldName + " does not exist in class " + object.getClass().getName() + ".");
		}
		return getMethodReturn(object, methodName) ;
	}	
	
	/**
	 * Retorna true caso exista um field com o mesmo nome e tipo na classe passada por par�metro,
	 * caso o par�metro fieldType esteja nulo, ele n�o ser� levado em conta  
	 * 
	 * @param clazz
	 * @param fieldName
	 * @param fieldType
	 * @return
	 */
	public static boolean fieldExists(Class clazz, String fieldName, Class fieldType) {
		for (Field f :getClassFields(clazz, true)) {
			if (f.getName().equals(fieldName) && (fieldType == null || fieldType.equals(f.getType()))) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Retorna a annotation presente na classe, ou nas superclasses até AppEntity.
	 * @author danilo sampaio
	 * @param clazz
	 * @param annotation
	 * @param root
	 * @return
	 */
	public static Annotation getHierarchicalAnnotation( Class clazz, Class annotation, Class root ){
		Class defaultRoot = root != null ? root : AppEntity.class;
		Annotation annot = null;
		Class currentClass = clazz;
		while ( currentClass != null && annot == null && !currentClass.equals(AppEntity.class) ){
			annot = currentClass.getAnnotation(annotation);
			currentClass = currentClass.getSuperclass();
		}
		return annot;
	}
	
	/**
	 * Verifica se a annotation está presente na classe, ou nas superclasses até AppEntity.
	 * @author danilo sampaio
	 * @param clazz
	 * @param annotation
	 * @param root
	 * @return
	 */
	public static Boolean isAnnotationPresentHierarchical( Class clazz, Class annotation, Class root ){
		Class defaultRoot = root != null ? root : AppEntity.class;
		Boolean annotationPresent = false;
		Class currentClass = clazz;
		while ( currentClass != null && !annotationPresent && !currentClass.equals(AppEntity.class) ){
			annotationPresent = currentClass.isAnnotationPresent(annotation);
			currentClass = currentClass.getSuperclass();
		}
		return annotationPresent;
	}
}